layout.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import Image from "next/image";
  2. import { Inter, Permanent_Marker } from "next/font/google";
  3. import { GeistSans } from "geist/font/sans";
  4. import { GeistMono } from "geist/font/mono";
  5. import { cn } from "@/shared/lib/utils";
  6. import { getServerUrl } from "@/shared/lib/server-url";
  7. import { FB_PIXEL_ID } from "@/shared/lib/facebook/fb-pixel";
  8. import { SiteConfig } from "@/shared/config/site-config";
  9. import { WorkoutSessionsSynchronizer } from "@/features/workout-session/ui/workout-sessions-synchronizer";
  10. import { ThemeSynchronizer } from "@/features/theme/ui/ThemeSynchronizer";
  11. import { Header } from "@/features/layout/Header";
  12. import { Footer } from "@/features/layout/Footer";
  13. import { TailwindIndicator } from "@/components/utils/TailwindIndicator";
  14. import { NextTopLoader } from "@/components/ui/next-top-loader";
  15. import { ServiceWorkerRegistration } from "@/components/pwa/ServiceWorkerRegistration";
  16. import { Providers } from "./providers";
  17. import type { ReactElement } from "react";
  18. import type { Metadata } from "next";
  19. import "@/shared/styles/globals.css";
  20. export const metadata: Metadata = {
  21. title: {
  22. default: SiteConfig.title,
  23. template: `%s | ${SiteConfig.title}`,
  24. },
  25. description: SiteConfig.description,
  26. metadataBase: new URL(getServerUrl()),
  27. robots: {
  28. index: true,
  29. follow: true,
  30. googleBot: {
  31. index: true,
  32. follow: true,
  33. "max-snippet": -1,
  34. "max-image-preview": "large",
  35. "max-video-preview": -1,
  36. },
  37. },
  38. openGraph: {
  39. title: SiteConfig.title,
  40. description: SiteConfig.description,
  41. url: getServerUrl(),
  42. siteName: SiteConfig.title,
  43. images: [
  44. {
  45. url: `${getServerUrl()}/images/default-og-image_fr.png`,
  46. width: 1200,
  47. height: 630,
  48. alt: SiteConfig.title,
  49. },
  50. {
  51. url: `${getServerUrl()}/images/default-og-image_en.png`,
  52. width: 1200,
  53. height: 630,
  54. alt: SiteConfig.title,
  55. },
  56. ],
  57. locale: "fr_FR",
  58. type: "website",
  59. },
  60. twitter: {
  61. card: "summary_large_image",
  62. site: "@workout_cool",
  63. title: SiteConfig.title,
  64. description: SiteConfig.description,
  65. images: [`${getServerUrl()}/images/default-og-image_fr.png`],
  66. },
  67. alternates: {
  68. canonical: "https://www.workout.cool",
  69. languages: {
  70. fr: "https://www.workout.cool/fr",
  71. en: "https://www.workout.cool/en",
  72. },
  73. },
  74. authors: [{ name: "Workout Cool", url: "https://www.workout.cool" }],
  75. icons: {
  76. icon: [
  77. { url: "/images/favicon-32x32.png", sizes: "32x32", type: "image/png" },
  78. { url: "/images/favicon-16x16.png", sizes: "16x16", type: "image/png" },
  79. { url: "/images/favicon.ico", type: "image/x-icon" },
  80. ],
  81. apple: "/apple-touch-icon.png",
  82. },
  83. };
  84. const inter = Inter({
  85. subsets: ["latin"],
  86. variable: "--font-inter",
  87. display: "swap",
  88. });
  89. const permanentMarker = Permanent_Marker({
  90. weight: "400",
  91. subsets: ["latin"],
  92. variable: "--font-permanent-marker",
  93. display: "swap",
  94. });
  95. export const preferredRegion = ["fra1", "sfo1", "iad1"];
  96. interface RootLayoutProps {
  97. params: Promise<{ locale: string }>;
  98. children: ReactElement;
  99. }
  100. export default async function RootLayout({ params, children }: RootLayoutProps) {
  101. const { locale } = await params;
  102. return (
  103. <>
  104. <html className="h-full" dir="ltr" lang={locale} suppressHydrationWarning>
  105. <head>
  106. <meta charSet="UTF-8" />
  107. <meta content="width=device-width, initial-scale=1, maximum-scale=1 viewport-fit=cover" name="viewport" />
  108. {/* PWA Meta Tags */}
  109. <meta content="yes" name="apple-mobile-web-app-capable" />
  110. <meta content="default" name="apple-mobile-web-app-status-bar-style" />
  111. <meta content="Workout Cool" name="apple-mobile-web-app-title" />
  112. <meta content="yes" name="mobile-web-app-capable" />
  113. <meta content="#FF5722" name="msapplication-TileColor" />
  114. <meta content="/android-chrome-192x192.png" name="msapplication-TileImage" />
  115. {/* PWA Manifest */}
  116. <link href="/manifest.json" rel="manifest" />
  117. {/* eslint-disable-next-line @next/next/no-page-custom-font */}
  118. <link as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="preload" />
  119. {/* Alternate hreflang for i18n */}
  120. <link href="https://www.workout.cool/fr" hrefLang="fr" rel="alternate" />
  121. <link href="https://www.workout.cool/en" hrefLang="en" rel="alternate" />
  122. {/* Theme color for PWA */}
  123. <meta content="#FF5722" name="theme-color" />
  124. {/* TODO: maybe add some ads ? */}
  125. <noscript>
  126. <Image
  127. alt="Facebook Pixel"
  128. height="1"
  129. src={`https://www.facebook.com/tr?id=${FB_PIXEL_ID}&ev=PageView&noscript=1`}
  130. style={{ display: "none" }}
  131. width="1"
  132. />
  133. </noscript>
  134. </head>
  135. <body
  136. className={cn(
  137. "flex items-center justify-center min-h-screen w-full p-8 max-sm:p-0 max-sm:min-h-full bg-base-200 dark:bg-[#18181b] dark:text-gray-200 antialiased",
  138. "bg-hero-light dark:bg-hero-dark",
  139. GeistMono.variable,
  140. GeistSans.variable,
  141. inter.variable,
  142. permanentMarker.variable,
  143. )}
  144. suppressHydrationWarning
  145. >
  146. <Providers locale={locale}>
  147. <ServiceWorkerRegistration />
  148. <WorkoutSessionsSynchronizer />
  149. <ThemeSynchronizer />
  150. <NextTopLoader color="#FF5722" delay={100} showSpinner={false} />
  151. {/* Main Card Container */}
  152. <div className="card w-full max-w-3xl min-h-[500px] max-h-[90vh] h-[80vh] bg-white dark:bg-[#232324] shadow-xl border border-base-200 dark:border-slate-700 flex flex-col justify-between overflow-hidden max-sm:rounded-none max-sm:h-full rounded-lg">
  153. <Header />
  154. <div className="flex-1 overflow-auto flex flex-col">{children}</div>
  155. <Footer />
  156. </div>
  157. <TailwindIndicator />
  158. </Providers>
  159. </body>
  160. </html>
  161. </>
  162. );
  163. }